feat: simulation foundation — models, ABC, factory, model registry, assets#84
Conversation
e5d70ca to
6bb80bd
Compare
yinsong1986
left a comment
There was a problem hiding this comment.
All review comments addressed. LGTM.
2594b1e to
2217260
Compare
|
Fixed in ef75a5d — added the missing sim = [
"robot_descriptions>=1.11.0,<2.0.0",
]
all = [
"strands-robots[groot-service]",
"strands-robots[lerobot]",
"strands-robots[sim]",
]The code in |
There was a problem hiding this comment.
Looking good, few small final comments.
Need to rebase after #83 is merged.
For all comments in this PR, we should examine common themes and include corrections for them in AGENTS.md so that future agent runs benefit from their lessons.
Address @awsarron review thread on manager.py — replace function-level lazy import of download.auto_download_robot with a module-level try/except that sets a sentinel (None) when the optional dependency is unavailable. This eliminates the circular-import design smell while preserving the same behavior: manager.py remains importable without robot_descriptions installed, and _auto_download_robot returns False immediately when the module is absent. Also fixes docstring reference: _safe_join → safe_join (canonical name).
|
Thanks for the thorough round-2 review @awsarron — addressed all 8 threads in
|
Verified against the code, only the findings that hold up on re-read: simulation/factory.py - _import_backend_class: raise a descriptive ImportError when a built-in backend module is declared but not installed, instead of letting the raw ModuleNotFoundError surface to users. The default "mujoco" path now points at an unimplemented module, so create_simulation() would previously crash with a cryptic trace — now the error names the backend, suggests the extra to install, and points at register_backend(). simulation/model_registry.py - Drop the import-time _URDF_SEARCH_PATHS constant (snapshotted Path.cwd() at import, broke pytest/notebook chdir flows). Use utils.get_search_paths() at every lookup — same function already used by assets.manager, so we dedupe too. - Demote the module-level 'Asset manager available' log from INFO at import to a lazy DEBUG on first resolution. assets/download.py - _copy_and_clean: filter ignored patterns at copytree() time instead of deleting from the destination afterwards. Previous behaviour would clobber a user's own README/LICENSE/images in their asset cache. assets/manager.py - Gate _user_asset_path resolution through safe_join() so a malicious or corrupted user_robots.json cannot sneak '..' into model_xml and escape the asset cache. Matches the trust boundary of the built-in registry path. registry/user_registry.py - register_robot() now fails closed when the asset directory does not exist (previously silently accepted invalid registrations). - Warn on alias collisions at registration time (shadows-canonical, shadows-existing-alias) instead of letting resolution order silently decide the winner. tests/test_registry_integrity.py - test_aliases_unique_across_registry - test_no_alias_shadows_canonical_name - test_hardware_only_robots_declare_lerobot_type tests/test_simulation_factory.py (new) - test_default_backend_missing_raises_import_error_with_guidance (regression test for the cryptic-ModuleNotFoundError fix) - test_register_backend_loader_must_be_callable - test_register_backend_rejects_duplicate_without_force .gitignore - Ignore .ideation/ (local scratchpad for idea cycles). All 338 unit tests pass. ruff + mypy clean.
a2be111 to
3a5c14b
Compare
- assets/manager.py: remove 'import os as _os' alias — just import os at top - simulation/models.py: clarify _model vs _data vs _backend_state roles in docstring so backend implementers know which to use (each has a distinct purpose: core model handle, core state handle, catch-all dict) - utils.py: decouple get_base_dir() from STRANDS_ASSETS_DIR. Introduce STRANDS_BASE_DIR as the explicit override for the base directory so user_robots.json no longer lands in an unexpected parent of the assets path. STRANDS_ASSETS_DIR now *only* moves the assets subtree. - registry/user_registry.py: update module docstring to reflect the new STRANDS_BASE_DIR semantics - tests/test_user_registry.py: update fixture + integration tests to set STRANDS_BASE_DIR and STRANDS_ASSETS_DIR independently; add tests asserting that STRANDS_ASSETS_DIR does *not* move the registry / base dir
3a5c14b to
fc89b1f
Compare
|
Thanks for the thorough review round @awsarron 🙏 — I've captured the 5 remaining items as a follow-up issue so they don't get lost, and going to merge this one so it stops being a rebase target for #85 and #86. Follow-up: #105 (added to the project board) Items tracked:
All polish / minor correctness — none of them block the foundation landing. Will pick them up in a small follow-up PR before starting the MuJoCo backend rebase. 👍 🤖 AI agent response. Strands Agents. Feedback welcome! |
Stale — all threads from this review have been resolved and addressed in subsequent commits. Superseded by @awsarron's approval on 2026-04-22.
Post-strands-labs#84 merge: SimWorld no longer carries MuJoCo-specific private fields (_xml, _robot_base_xml, _recording, _trajectory, _dataset_recorder, _tmpdir, _push_to_hub). These are MuJoCo backend implementation details and now live in world._backend_state, as the SimWorld docstring requests (prefer _backend_state over new fields). Migrated call sites: - mjcf_builder.py: tmpdir - policy_runner.py: recording, trajectory, dataset_recorder - recording.py: recording, trajectory, dataset_recorder, push_to_hub - scene_ops.py: robot_base_xml - simulation.py: xml, robot_base_xml, recording, trajectory Reads use dict[] where preceded by a guard that guarantees initialization (e.g. start_recording() sets before policy_runner reads), and .get() with sensible defaults where the key may be unset. Tests: 392 passed, 2 skipped (5 pre-existing test_path_validation failures are on main too — unrelated). Lint: ruff + mypy clean on 75 source files.
… registration Add Newton GPU simulation backend stub that implements SimEngine ABC. No GPU dependencies at import time — warp/newton are lazy-loaded. New files: - simulation/newton/__init__.py — lazy loading, no warp import - simulation/newton/config.py — NewtonConfig dataclass (7 solvers) - simulation/newton/simulation.py — NewtonSimulation(SimEngine) skeleton Modified files: - simulation/factory.py — register newton + aliases (warp, wp) - pyproject.toml — add [newton] extras (warp-lang, newton-sim) All 12 required SimEngine methods stubbed with NotImplementedError. Newton-specific extensions (replicate, diffsim, IK, cloth, etc.) stubbed. Factory kwargs flow through to NewtonConfig. Tests: 81 passed (config validation, factory round-trip, lazy import, ABC conformance, all stub methods verified). Part 1 of 7-PR Newton backend series (issue #314). Depends on PR strands-labs#84 (SimEngine ABC).
…spec dispatcher
Bug: Simulation._dispatch_action filtered kwargs through a hardcoded
whitelist that omitted observation_mapping, action_mapping, data_config,
host, port, api_token, trust_remote_code, actions_per_step,
use_processor, processor_overrides, and any other policy-specific kwarg.
Agents could not wire a policy (GR00T, SmolVLA, lerobot_local) to a
simulated robot through the AgentTool interface — sim joint names and
canonical model keys never got reconciled, breaking sim↔real transfer.
Fix:
- simulation.py::_dispatch_action — replace the whitelist with a
mapping-aware passthrough: for methods that declare **policy_kwargs,
forward every input field that isn't already matched to a named
parameter. Actions without **kwargs stay strict.
- tool_spec.json — advertise observation_mapping, action_mapping, host,
port, api_token, trust_remote_code, actions_per_step, use_processor,
processor_overrides, device so agents can discover and use them.
- tests/test_tool_spec_dispatch_policy_kwargs.py — 5 regression tests
pinning the forwarding for run_policy / eval_policy / start_policy
and verifying non-policy actions stay strict.
End-to-end validation (MacBook Pro M-series, MPS):
- create_world → add_robot(so100) → add_object(red_cube)
→ add_camera(camera1) → add_camera(camera2)
→ run_policy(policy_provider='lerobot_local',
pretrained_name_or_path='lerobot/smolvla_base',
device='mps',
observation_mapping={'camera1': 'observation.images.camera1',
'camera2': 'observation.images.camera2',
'joint_position': 'observation.state'},
action_mapping={'action': 'joint_position'})
- SmolVLA downloaded, loaded on MPS, produced actions, sim stepped.
2 control steps / 25.4s wall → proves the full chain works.
Quality:
- ruff + mypy: clean (77 files)
- hatch run test: 5/5 new tests pass; only pre-existing
test_path_validation failures remain (noted by author on strands-labs#84).
Post-strands-labs#84 merge: SimWorld no longer carries MuJoCo-specific private fields (_xml, _robot_base_xml, _recording, _trajectory, _dataset_recorder, _tmpdir, _push_to_hub). These are MuJoCo backend implementation details and now live in world._backend_state, as the SimWorld docstring requests (prefer _backend_state over new fields). Migrated call sites: - mjcf_builder.py: tmpdir - policy_runner.py: recording, trajectory, dataset_recorder - recording.py: recording, trajectory, dataset_recorder, push_to_hub - scene_ops.py: robot_base_xml - simulation.py: xml, robot_base_xml, recording, trajectory Reads use dict[] where preceded by a guard that guarantees initialization (e.g. start_recording() sets before policy_runner reads), and .get() with sensible defaults where the key may be unset. Tests: 392 passed, 2 skipped (5 pre-existing test_path_validation failures are on main too — unrelated). Lint: ruff + mypy clean on 75 source files.
…spec dispatcher
Bug: Simulation._dispatch_action filtered kwargs through a hardcoded
whitelist that omitted observation_mapping, action_mapping, data_config,
host, port, api_token, trust_remote_code, actions_per_step,
use_processor, processor_overrides, and any other policy-specific kwarg.
Agents could not wire a policy (GR00T, SmolVLA, lerobot_local) to a
simulated robot through the AgentTool interface — sim joint names and
canonical model keys never got reconciled, breaking sim↔real transfer.
Fix:
- simulation.py::_dispatch_action — replace the whitelist with a
mapping-aware passthrough: for methods that declare **policy_kwargs,
forward every input field that isn't already matched to a named
parameter. Actions without **kwargs stay strict.
- tool_spec.json — advertise observation_mapping, action_mapping, host,
port, api_token, trust_remote_code, actions_per_step, use_processor,
processor_overrides, device so agents can discover and use them.
- tests/test_tool_spec_dispatch_policy_kwargs.py — 5 regression tests
pinning the forwarding for run_policy / eval_policy / start_policy
and verifying non-policy actions stay strict.
End-to-end validation (MacBook Pro M-series, MPS):
- create_world → add_robot(so100) → add_object(red_cube)
→ add_camera(camera1) → add_camera(camera2)
→ run_policy(policy_provider='lerobot_local',
pretrained_name_or_path='lerobot/smolvla_base',
device='mps',
observation_mapping={'camera1': 'observation.images.camera1',
'camera2': 'observation.images.camera2',
'joint_position': 'observation.state'},
action_mapping={'action': 'joint_position'})
- SmolVLA downloaded, loaded on MPS, produced actions, sim stepped.
2 control steps / 25.4s wall → proves the full chain works.
Quality:
- ruff + mypy: clean (77 files)
- hatch run test: 5/5 new tests pass; only pre-existing
test_path_validation failures remain (noted by author on strands-labs#84).
TL;DR
Simulation foundation layer — dataclasses,
SimEngineABC, pluggable backend factory, URDF/MJCF model registry, asset manager with auto-download, user robot registration, and@toolfor asset downloads. Expands robot registry from 38 → 68 robots. Pure Python, no MuJoCo dependency.Rebased on
main(post-#83). All 50 review threads resolved. All CI green.What changed
Simulation abstractions (
strands_robots/simulation/)models.pySimWorld,SimRobot,SimObject,SimCamera,TrajectoryStep,SimStatus. Backend-agnostic — engine state stored in generic_backend_state: dict(no MuJoCo-specific fields).base.pySimEngineABC — 12 required abstract methods + 4 optional (raiseNotImplementedError). Context manager protocol.__del__logs cleanup errors instead of silencing.factory.pycreate_simulation()+register_backend()with duplicate protection (ValueErroron conflict), built-in alias protection,force=Truefor intentional overwrites.model_registry.pySTRANDS_ASSETS_DIR→~/.strands_robots/assets/→ CWD →robot_descriptionsfallback. Logs asset manager availability.__init__.py__getattr__lazy loading. No MuJoCo references.Assets (
strands_robots/assets/)__init__.pymanager.py_safe_join()path-traversal protection,_resolve_candidates()helper. Search paths documented. No bundled assets — everything fromrobot_descriptions.download.pyrobot_descriptions→ git clone fallback._shallow_clone()validates URLs via_ALLOWED_CLONE_URL_RE(HTTPS github.com only).Tools (
strands_robots/tools/)download_assets.py@toolwrapper (~78 lines) — delegates toassets.download. No duplicated logic.Registry (
strands_robots/registry/)user_registry.pyregister_robot()/unregister_robot()— persisted to~/.strands_robots/user_robots.json. Security warning on docstring re: MJCF code execution risk.loader.pyrobots.json. Publicinvalidate_cache()API (no private imports).robots.json__init__.pyregister_robot,unregister_robot,list_user_robots,invalidate_cache.Other
utils.pyget_base_dir(),get_assets_dir(),resolve_asset_path()— single source of truth.README.mdpyproject.toml[sim]extra withrobot_descriptionsdependency.Design decisions
SimEngine ABC
Asset resolution order
Customer assets always win over defaults. Single env var:
STRANDS_ASSETS_DIR.Security
_safe_join()in bothmanager.pyanddownload.py_shallow_clone()rejects non-HTTPS/non-github.com URLsregister_robot(): Library-only function, not exposed as@tool. Docstring warns about MJCF code execution risk if ever surfaced to agents.Testing
Review history
Key changes since CHANGES_REQUESTED:
main(post-chore: modernize build system — uv lockfile, Python >=3.12 #83, Python ≥3.12 + uv)SimulationBackend→SimEnginetools/→assets/download.py(tools as thin wrappers)SimWorld→ generic_backend_stateregister_robot()docstringUnblocks #85 (MuJoCo backend) and #86 (Robot factory).